"""CLI argument definitions for datamodel-codegen.

Defines the ArgumentParser and all command-line options organized into groups:
base options, typing customization, field customization, model customization,
template customization, OpenAPI-specific options, and general options.
"""

from __future__ import annotations

import json
import locale
from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction, HelpFormatter, Namespace
from operator import attrgetter
from pathlib import Path
from typing import TYPE_CHECKING, cast

from datamodel_code_generator import (
    DEFAULT_SHARED_MODULE_NAME,
    AllExportsCollisionStrategy,
    AllExportsScope,
    DataclassArguments,
    DataModelType,
    InputFileType,
    OpenAPIScope,
    ReadOnlyWriteOnlyModelType,
    ReuseScope,
)
from datamodel_code_generator.format import DatetimeClassType, Formatter, PythonVersion
from datamodel_code_generator.model.pydantic_v2 import UnionMode
from datamodel_code_generator.parser import LiteralType
from datamodel_code_generator.types import StrictTypes

if TYPE_CHECKING:
    from argparse import Action
    from collections.abc import Iterable

DEFAULT_ENCODING = locale.getpreferredencoding()

namespace = Namespace(no_color=False)


def _dataclass_arguments(value: str) -> DataclassArguments:
    """Parse JSON string and validate it as DataclassArguments."""
    try:
        result = json.loads(value)
    except json.JSONDecodeError as e:
        msg = f"Invalid JSON: {e}"
        raise ArgumentTypeError(msg) from e
    if not isinstance(result, dict):
        msg = f"Expected a JSON dictionary, got {type(result).__name__}"
        raise ArgumentTypeError(msg)
    valid_keys = set(DataclassArguments.__annotations__.keys())
    invalid_keys = set(result.keys()) - valid_keys
    if invalid_keys:
        msg = f"Invalid keys: {invalid_keys}. Valid keys are: {valid_keys}"
        raise ArgumentTypeError(msg)
    for key, val in result.items():
        if not isinstance(val, bool):
            msg = f"Expected bool for '{key}', got {type(val).__name__}"
            raise ArgumentTypeError(msg)
    return cast("DataclassArguments", result)


class SortingHelpFormatter(HelpFormatter):
    """Help formatter that sorts arguments and adds color to section headers."""

    def _bold_cyan(self, text: str) -> str:  # noqa: PLR6301
        """Wrap text in ANSI bold cyan escape codes."""
        return f"\x1b[36;1m{text}\x1b[0m"

    def add_arguments(self, actions: Iterable[Action]) -> None:
        """Add arguments sorted by option strings."""
        actions = sorted(actions, key=attrgetter("option_strings"))
        super().add_arguments(actions)

    def start_section(self, heading: str | None) -> None:
        """Start a section with optional colored heading."""
        return super().start_section(heading if namespace.no_color or not heading else self._bold_cyan(heading))


arg_parser = ArgumentParser(
    usage="\n  datamodel-codegen [options]",
    description="Generate Python data models from schema definitions or structured data",
    formatter_class=SortingHelpFormatter,
    add_help=False,
)

base_options = arg_parser.add_argument_group("Options")
typing_options = arg_parser.add_argument_group("Typing customization")
field_options = arg_parser.add_argument_group("Field customization")
model_options = arg_parser.add_argument_group("Model customization")
extra_fields_model_options = model_options.add_mutually_exclusive_group()
template_options = arg_parser.add_argument_group("Template customization")
openapi_options = arg_parser.add_argument_group("OpenAPI-only options")
general_options = arg_parser.add_argument_group("General options")

# ======================================================================================
# Base options for input/output
# ======================================================================================
base_options.add_argument(
    "--http-headers",
    nargs="+",
    metavar="HTTP_HEADER",
    help='Set headers in HTTP requests to the remote host. (example: "Authorization: Basic dXNlcjpwYXNz")',
)
base_options.add_argument(
    "--http-query-parameters",
    nargs="+",
    metavar="HTTP_QUERY_PARAMETERS",
    help='Set query parameters in HTTP requests to the remote host. (example: "ref=branch")',
)
base_options.add_argument(
    "--http-ignore-tls",
    help="Disable verification of the remote host's TLS certificate",
    action="store_true",
    default=None,
)
base_options.add_argument(
    "--input",
    help="Input file/directory (default: stdin)",
)
base_options.add_argument(
    "--input-file-type",
    help="Input file type (default: auto)",
    choices=[i.value for i in InputFileType],
)
base_options.add_argument(
    "--output",
    help="Output file (default: stdout)",
)
base_options.add_argument(
    "--output-model-type",
    help="Output model type (default: pydantic.BaseModel)",
    choices=[i.value for i in DataModelType],
)
base_options.add_argument(
    "--url",
    help="Input file URL. `--input` is ignored when `--url` is used",
)

# ======================================================================================
# Customization options for generated models
# ======================================================================================
extra_fields_model_options.add_argument(
    "--allow-extra-fields",
    help="Deprecated: Allow passing extra fields. This flag is deprecated. Use `--extra-fields=allow` instead.",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--allow-population-by-field-name",
    help="Allow population by field name",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--class-name",
    help="Set class name of root model",
    default=None,
)
model_options.add_argument(
    "--collapse-root-models",
    action="store_true",
    default=None,
    help="Models generated with a root-type field will be merged into the models using that root-type model",
)
model_options.add_argument(
    "--skip-root-model",
    action="store_true",
    default=None,
    help="Skip generating the model for the root schema element",
)
model_options.add_argument(
    "--disable-appending-item-suffix",
    help="Disable appending `Item` suffix to model name in an array",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--disable-timestamp",
    help="Disable timestamp on file headers",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--enable-faux-immutability",
    help="Enable faux immutability",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--enable-version-header",
    help="Enable package version on file headers",
    action="store_true",
    default=None,
)
extra_fields_model_options.add_argument(
    "--extra-fields",
    help="Set the generated models to allow, forbid, or ignore extra fields.",
    choices=["allow", "ignore", "forbid"],
    default=None,
)
model_options.add_argument(
    "--keep-model-order",
    help="Keep generated models' order",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--keyword-only",
    help="Defined models as keyword only (for example dataclass(kw_only=True)).",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--frozen-dataclasses",
    help="Generate frozen dataclasses (dataclass(frozen=True)). Only applies to dataclass output.",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--dataclass-arguments",
    type=_dataclass_arguments,
    default=None,
    help=(
        "Custom dataclass arguments as a JSON dictionary, "
        'e.g. \'{"frozen": true, "kw_only": true}\'. '
        "Overrides --frozen-dataclasses and similar flags."
    ),
)
model_options.add_argument(
    "--reuse-model",
    help="Reuse models on the field when a module has the model with the same content",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--reuse-scope",
    help="Scope for model reuse deduplication: module (per-file, default) or tree (cross-file with shared module). "
    "Only effective when --reuse-model is set.",
    choices=[s.value for s in ReuseScope],
    default=None,
)
model_options.add_argument(
    "--shared-module-name",
    help=f'Name of the shared module for --reuse-scope=tree (default: "{DEFAULT_SHARED_MODULE_NAME}"). '
    f'Use this option if your schema has a file named "{DEFAULT_SHARED_MODULE_NAME}".',
    default=None,
)
model_options.add_argument(
    "--target-python-version",
    help="target python version",
    choices=[v.value for v in PythonVersion],
)
model_options.add_argument(
    "--treat-dot-as-module",
    help="treat dotted module names as modules",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--use-schema-description",
    help="Use schema description to populate class docstring",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--use-title-as-name",
    help="use titles as class names of models",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--use-pendulum",
    help="use pendulum instead of datetime",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--use-exact-imports",
    help='import exact types instead of modules, for example: "from .foo import Bar" instead of '
    '"from . import foo" with "foo.Bar"',
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--output-datetime-class",
    help="Choose Datetime class between AwareDatetime, NaiveDatetime or datetime. "
    "Each output model has its default mapping (for example pydantic: datetime, dataclass: str, ...)",
    choices=[i.value for i in DatetimeClassType],
    default=None,
)
model_options.add_argument(
    "--parent-scoped-naming",
    help="Set name of models defined inline from the parent model",
    action="store_true",
    default=None,
)
model_options.add_argument(
    "--all-exports-scope",
    help="Generate __all__ in __init__.py with re-exports. "
    "'children': export from direct child modules only. "
    "'recursive': export from all descendant modules.",
    choices=[s.value for s in AllExportsScope],
    default=None,
)
model_options.add_argument(
    "--all-exports-collision-strategy",
    help="Strategy for name collisions when using --all-exports-scope=recursive. "
    "'error': raise an error (default). "
    "'minimal-prefix': add module prefix only to colliding names. "
    "'full-prefix': add full module path prefix to colliding names.",
    choices=[s.value for s in AllExportsCollisionStrategy],
    default=None,
)

# ======================================================================================
# Typing options for generated models
# ======================================================================================
typing_options.add_argument(
    "--base-class",
    help="Base Class (default: pydantic.BaseModel)",
    type=str,
)
typing_options.add_argument(
    "--enum-field-as-literal",
    help="Parse enum field as literal. "
    "all: all enum field type are Literal. "
    "one: field type is Literal when an enum has only one possible value",
    choices=[lt.value for lt in LiteralType],
    default=None,
)
typing_options.add_argument(
    "--field-constraints",
    help="Use field constraints and not con* annotations",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--set-default-enum-member",
    help="Set enum members as default values for enum field",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--strict-types",
    help="Use strict types",
    choices=[t.value for t in StrictTypes],
    nargs="+",
)
typing_options.add_argument(
    "--use-annotated",
    help="Use typing.Annotated for Field(). Also, `--field-constraints` option will be enabled.",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-serialize-as-any",
    help="Use pydantic.SerializeAsAny for fields with types that have subtypes (Pydantic v2 only)",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-generic-container-types",
    help="Use generic container types for type hinting (typing.Sequence, typing.Mapping). "
    "If `--use-standard-collections` option is set, then import from collections.abc instead of typing",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-non-positive-negative-number-constrained-types",
    help="Use the Non{Positive,Negative}{FloatInt} types instead of the corresponding con* constrained types.",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-one-literal-as-default",
    help="Use one literal as default value for one literal field",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-enum-values-in-discriminator",
    help="Use enum member literals in discriminator fields instead of string literals",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-standard-collections",
    help="Use standard collections for type hinting (list, dict)",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-subclass-enum",
    help="Define generic Enum class as subclass with field type when enum has type (int, float, bytes, str)",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-specialized-enum",
    help="Don't use specialized Enum class (StrEnum, IntEnum) even if the target Python version supports it",
    action=BooleanOptionalAction,
    default=None,
)
typing_options.add_argument(
    "--use-union-operator",
    help="Use | operator for Union type (PEP 604).",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-unique-items-as-set",
    help="define field type as `set` when the field attribute has `uniqueItems`",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--use-type-alias",
    help="Use TypeAlias instead of root models (experimental)",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--disable-future-imports",
    help="Disable __future__ imports",
    action="store_true",
    default=None,
)
typing_options.add_argument(
    "--type-mappings",
    help="Override default type mappings. "
    'Format: "type+format=target" (e.g., "string+binary=string" to map binary format to string type) '
    'or "format=target" (e.g., "binary=string"). '
    "Can be specified multiple times.",
    nargs="+",
    type=str,
    default=None,
)

# ======================================================================================
# Customization options for generated model fields
# ======================================================================================
field_options.add_argument(
    "--capitalise-enum-members",
    "--capitalize-enum-members",
    help="Capitalize field names on enum",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--empty-enum-field-name",
    help="Set field name when enum value is empty (default:  `_`)",
    default=None,
)
field_options.add_argument(
    "--field-extra-keys",
    help="Add extra keys to field parameters",
    type=str,
    nargs="+",
)
field_options.add_argument(
    "--field-extra-keys-without-x-prefix",
    help="Add extra keys with `x-` prefix to field parameters. The extra keys are stripped of the `x-` prefix.",
    type=str,
    nargs="+",
)
field_options.add_argument(
    "--field-include-all-keys",
    help="Add all keys to field parameters",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--force-optional",
    help="Force optional for required fields",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--original-field-name-delimiter",
    help="Set delimiter to convert to snake case. This option only can be used with --snake-case-field (default: `_` )",
    default=None,
)
field_options.add_argument(
    "--remove-special-field-name-prefix",
    help="Remove field name prefix if it has a special meaning e.g. underscores",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--snake-case-field",
    help="Change camel-case field name to snake-case",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--special-field-name-prefix",
    help="Set field name prefix when first character can't be used as Python field name (default:  `field`)",
    default=None,
)
field_options.add_argument(
    "--strip-default-none",
    help="Strip default None on fields",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--use-default",
    help="Use default value even if a field is required",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--use-default-kwarg",
    action="store_true",
    help="Use `default=` instead of a positional argument for Fields that have default values.",
    default=None,
)
field_options.add_argument(
    "--use-field-description",
    help="Use schema description to populate field docstring",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--use-attribute-docstrings",
    help="Set use_attribute_docstrings=True in Pydantic v2 ConfigDict",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--use-inline-field-description",
    help="Use schema description to populate field docstring as inline docstring",
    action="store_true",
    default=None,
)
field_options.add_argument(
    "--union-mode",
    help="Union mode for only pydantic v2 field",
    choices=[u.value for u in UnionMode],
    default=None,
)
field_options.add_argument(
    "--no-alias",
    help="""Do not add a field alias. E.g., if --snake-case-field is used along with a base class, which has an
            alias_generator""",
    action="store_true",
    default=None,
)

# ======================================================================================
# Options for templating output
# ======================================================================================
template_options.add_argument(
    "--aliases",
    help="Alias mapping file (JSON) for renaming fields. "
    "Supports hierarchical formats: "
    "Flat: {'field': 'alias'} applies to all occurrences. "
    "Scoped: {'ClassName.field': 'alias'} applies to specific class. "
    "Priority: scoped > flat. "
    "Example: {'User.name': 'user_name', 'Address.name': 'addr_name', 'id': 'id_'}",
    type=Path,
)
template_options.add_argument(
    "--custom-file-header",
    help="Custom file header",
    type=str,
    default=None,
)
template_options.add_argument(
    "--custom-file-header-path",
    help="Custom file header file path",
    default=None,
    type=str,
)
template_options.add_argument(
    "--custom-template-dir",
    help="Custom template directory",
    type=str,
)
template_options.add_argument(
    "--encoding",
    help=f"The encoding of input and output (default: {DEFAULT_ENCODING})",
    default=None,
)
template_options.add_argument(
    "--extra-template-data",
    help="Extra template data for output models. Input is supposed to be a json/yaml file. "
    "For OpenAPI and Jsonschema the keys are the spec path of the object, or the name of the object if you want to "
    "apply the template data to multiple objects with the same name. "
    "If you are using another input file type (e.g. GraphQL), the key is the name of the object. "
    "The value is a dictionary of the template data to add.",
    type=Path,
)
template_options.add_argument(
    "--use-double-quotes",
    action="store_true",
    default=None,
    help="Model generated with double quotes. Single quotes or "
    "your black config skip_string_normalization value will be used without this option.",
)
template_options.add_argument(
    "--wrap-string-literal",
    help="Wrap string literal by using black `experimental-string-processing` option (require black 20.8b0 or later)",
    action="store_true",
    default=None,
)
base_options.add_argument(
    "--additional-imports",
    help='Custom imports for output (delimited list input). For example "datetime.date,datetime.datetime"',
    type=str,
    default=None,
)
base_options.add_argument(
    "--formatters",
    help="Formatters for output (default: [black, isort])",
    choices=[f.value for f in Formatter],
    nargs="+",
    default=None,
)
base_options.add_argument(
    "--custom-formatters",
    help="List of modules with custom formatter (delimited list input).",
    type=str,
    default=None,
)
template_options.add_argument(
    "--custom-formatters-kwargs",
    help="A file with kwargs for custom formatters.",
    type=Path,
)

# ======================================================================================
# Options specific to OpenAPI input schemas
# ======================================================================================
openapi_options.add_argument(
    "--openapi-scopes",
    help="Scopes of OpenAPI model generation (default: schemas)",
    choices=[o.value for o in OpenAPIScope],
    nargs="+",
    default=None,
)
openapi_options.add_argument(
    "--strict-nullable",
    help="Treat default field as a non-nullable field (Only OpenAPI)",
    action="store_true",
    default=None,
)
openapi_options.add_argument(
    "--use-operation-id-as-name",
    help="use operation id of OpenAPI as class names of models",
    action="store_true",
    default=None,
)
openapi_options.add_argument(
    "--include-path-parameters",
    help="Include path parameters in generated parameter models in addition to query parameters (Only OpenAPI)",
    action="store_true",
    default=None,
)
openapi_options.add_argument(
    "--validation",
    help="Deprecated: Enable validation (Only OpenAPI). this option is deprecated. it will be removed in future "
    "releases",
    action="store_true",
    default=None,
)
openapi_options.add_argument(
    "--read-only-write-only-model-type",
    help="Model generation for readOnly/writeOnly fields: "
    "'request-response' = Request/Response models only (no base model), "
    "'all' = Base + Request + Response models.",
    choices=[e.value for e in ReadOnlyWriteOnlyModelType],
    default=None,
)

# ======================================================================================
# General options
# ======================================================================================
general_options.add_argument(
    "--check",
    action="store_true",
    default=None,
    help="Verify generated files are up-to-date without modifying them. "
    "Exits with code 1 if differences found, 0 if up-to-date. "
    "Useful for CI to ensure generated code is committed.",
)
general_options.add_argument(
    "--debug",
    help="show debug message (require \"debug\". `$ pip install 'datamodel-code-generator[debug]'`)",
    action="store_true",
    default=None,
)
general_options.add_argument(
    "--disable-warnings",
    help="disable warnings",
    action="store_true",
    default=None,
)
general_options.add_argument(
    "-h",
    "--help",
    action="help",
    default="==SUPPRESS==",
    help="show this help message and exit",
)
general_options.add_argument(
    "--no-color",
    action="store_true",
    default=False,
    help="disable colorized output",
)
general_options.add_argument(
    "--generate-pyproject-config",
    action="store_true",
    default=None,
    help="Generate pyproject.toml configuration from the provided CLI arguments and exit",
)
general_options.add_argument(
    "--generate-cli-command",
    action="store_true",
    default=None,
    help="Generate CLI command from pyproject.toml configuration and exit",
)
general_options.add_argument(
    "--ignore-pyproject",
    action="store_true",
    default=False,
    help="Ignore pyproject.toml configuration",
)
general_options.add_argument(
    "--profile",
    help="Use a named profile from pyproject.toml [tool.datamodel-codegen.profiles.<name>]",
    default=None,
)
general_options.add_argument(
    "--version",
    action="store_true",
    help="show version",
)

__all__ = [
    "DEFAULT_ENCODING",
    "arg_parser",
    "namespace",
]
